前幾篇文章中,我們定義好資料儲存的結構作為 initialState,以及資料更新的邏輯 reducer,接下來進入元件的部分。
所有的元件都是從根元件長出來的,所以我們要先建立根元件取名叫做 App。首先在 src 資料夾中新增 components 資料夾,components 資料夾中再新增 App 資料夾,最後在 App 資料夾底下新增名稱為 App.js 的檔案:
import React from 'react';
const App = () => {
  return (
    <div>
      <h1>我的專案</h1>
    </div>
  );
};
export default App;
然後在 index.js 也就是程式進入點,引入 App 根元件並且掛載到 real DOM上:
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App/App";
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
畫面上應該可以看到 我的專案 的文字。
接下來要使用 useReducer 讓標題的文字改為從 state 中取得。在 src/components/App 資料夾底下新增 reducer.js 檔案,然後把之前定義好的 initialState 和 reducer 加進去,記得在最後要將它們 export 出來:
import shortid from "shortid";
const initialState = {
  title: "我的專案",
  boards: {
    ids: [],
    byId: {}
  },
  cards: {
    byId: {}
  }
};
const reducer = (state, action) => {
  switch (action.type) {
    case "CHANGE_TITLE": {
      const { title } = action.payload;
      return {
        ...state,
        title
      };
    }
    case "ADD_BOARD": {
      const boardId = shortid.generate();
      const { boardName } = action.payload;
      return {
        ...state,
        boards: {
          ids: [...state.boards.ids, boardId],
          byId: {
            ...state.boards.byId,
            [boardId]: {
              name: boardName,
              cardIds: []
            }
          }
        }
      };
    }
    case "CHANGE_BOARD_NAME": {
      const { boardId, boardName } = action.payload;
      return {
        ...state,
        boards: {
          ...state.boards,
          byId: {
            ...state.boards.byId,
            [boardId]: {
              ...state.boards.byId[boardId],
              name: boardName
            }
          }
        }
      };
    }
    case "MOVE_BOARD": {
      const { draggingBoardId, targetBoardIndex } = action.payload;
      const newBoardsIds = [...state.boards.ids];
      const sourceBoardIndex = state.boards.ids.findIndex(
        boardId => boardId === draggingBoardId
      );
      newBoardsIds.splice(sourceBoardIndex, 1);
      newBoardsIds.splice(targetBoardIndex, 0, draggingBoardId);
      return {
        ...state,
        boards: {
          ...state.boards,
          ids: newBoardsIds
        }
      };
    }
    case "REMOVE_BOARD": {
      const { boardId } = action.payload;
      const newBoardsIds = state.boards.ids.filter(id => id !== boardId);
      const newBoardsById = { ...state.boards.byId };
      delete newBoardsById[boardId];
      const newCardsById = { ...state.cards.byId };
      state.boards.byId[boardId].cardIds.forEach(cardId => {
        delete newCardsById[cardId];
      });
      return {
        ...state,
        boards: {
          ids: newBoardsIds,
          byId: newBoardsById
        },
        cards: {
          byId: newCardsById
        }
      };
    }
    case "ADD_CARD": {
      const cardId = shortid.generate();
      const { boardId, cardValue } = action.payload;
      return {
        ...state,
        boards: {
          ...state.boards,
          byId: {
            ...state.boards.byId,
            [boardId]: {
              ...state.boards.byId[boardId],
              cardIds: [...state.boards.byId[boardId].cardIds, cardId]
            }
          }
        },
        cards: {
          byId: {
            ...state.cards.byId,
            [cardId]: cardValue
          }
        }
      };
    }
    case "CHANGE_CARD_VALUE": {
      const { cardId, cardValue } = action.payload;
      return {
        ...state,
        cards: {
          byId: {
            ...state.cards.byId,
            [cardId]: cardValue
          }
        }
      };
    }
    case "MOVE_CARD": {
      const { draggingCardId, targetBoardId, targetCardIndex } = action.payload;
      const sourceBoardId = Object.keys(state.boards.byId).find(boardId => {
        return state.boards.byId[boardId].cardIds.find(
          cardId => cardId === draggingCardId
        );
      });
      const sourceBoardCardIds = [...state.boards.byId[sourceBoardId].cardIds];
      const targetBoardCardIds =
        sourceBoardId === targetBoardId
          ? sourceBoardCardIds
          : [...state.boards.byId[targetBoardId].cardIds];
      const sourceCardIndex = sourceBoardCardIds.findIndex(
        cardId => cardId === draggingCardId
      );
      sourceBoardCardIds.splice(sourceCardIndex, 1);
      targetBoardCardIds.splice(targetCardIndex, 0, draggingCardId);
      return {
        ...state,
        boards: {
          ...state.boards,
          byId: {
            ...state.boards.byId,
            [sourceBoardId]: {
              ...state.boards.byId[sourceBoardId],
              cardIds: sourceBoardCardIds
            },
            [targetBoardId]: {
              ...state.boards.byId[targetBoardId],
              cardIds: targetBoardCardIds
            }
          }
        }
      };
    }
    case "REMOVE_CARD": {
      const { draggingCardId } = action.payload;
      const sourceBoardId = Object.keys(state.boards.byId).find(boardId => {
        return state.boards.byId[boardId].cardIds.find(
          cardId => cardId === draggingCardId
        );
      });
      const sourceBoardCardIds = [...state.boards.byId[sourceBoardId].cardIds];
      const sourceCardIndex = sourceBoardCardIds.findIndex(
        cardId => cardId === draggingCardId
      );
      sourceBoardCardIds.splice(sourceCardIndex, 1);
      const newCardsById = { ...state.cards.byId };
      delete newCardsById[draggingCardId];
      return {
        ...state,
        boards: {
          ...state.boards,
          byId: {
            ...state.boards.byId,
            [sourceBoardId]: {
              ...state.boards.byId[sourceBoardId],
              cardIds: sourceBoardCardIds
            }
          }
        },
        cards: {
          byId: newCardsById
        }
      };
    }
    default:
      throw new Error();
  }
};
export { reducer, initialState };
接著在 App.js 引入並且傳入 useReducer 呼叫,將原本寫死的標題改成從 state 取得:
import React from 'react';
import { reducer, initialState } from './reducer'; 
const App = () => {
  const [state, dispatch] = React.useReducer(reducer, initialState);
  
  return (
    <div>
      <h1>{state.title}</h1>
    </div>
  );
};
export default App;
在畫面上應該一樣會看到 我的專案。
接著再套用已經寫好的全局樣式。新增 index.scss 檔案,並且在 index.js 中引入:
* {
  font-size: 16px;
}
body {
  margin: 0;
  padding: 16px;
  background-color: gray;
}
button {
  outline: none;
  border: none;
  background: none;
  cursor: pointer;
}
input {
  padding: 8px;
}
h1, h2 {
  margin: 0;
}
import React from "react";
import ReactDOM from "react-dom";
import App from "./components/App/App";
import "./index.scss";
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
最後的畫面:

加入編輯標題的功能。